﻿using System;
using System.Collections.Generic;
using System.Text;
using MLSharp.Utilities;

namespace MLSharp
{
	/// <summary>
	/// Provides a basic implementation of 
	/// <see cref="IDataSet"/> that can be populated
	/// programmatically.
	/// </summary>
	/// <remarks>
	/// Implementors of <see cref="IDataSet"/> can inherit from 
	/// this base class to get some validation functionality for
	/// free.
	/// </remarks>
	public class BasicDataSet : IDataSet
	{
		#region Private Fields

		/// <summary>
		/// The attributes of the data set.
		/// </summary>
		private readonly IList<DataAttribute> mAttributes;

		/// <summary>
		/// The instances of the data set.
		/// </summary>
		private readonly IList<Instance> mInstances;

		#endregion

		#region Private Static Methods

		/// <summary>
		/// Validates a value based on the type of the attribute.
		/// </summary>
		/// <param name="attribute"></param>
		/// <param name="value"></param>
		private static void ValidateAttributeValue(DataAttribute attribute, double value)
		{
			//Null values are ok as they just indicate missing data, so skip them.
			if (double.IsNaN(value))
				return;

			switch (attribute.Type)
			{
				case AttributeType.Discrete:
					ValidateDiscrete(attribute, value);
					break;
				case AttributeType.Set:
					ValidateSet(attribute.Name, attribute.PossibleValues, value);
					break;
			}
		}

		/// <summary>
		/// Validates that new values for discrete variables are within the set of
		/// allowed values.
		/// </summary>
		/// <param name="attribute"></param>
		/// <param name="value"></param>
		private static void ValidateDiscrete(DataAttribute attribute, double value)
		{
			//If the value has been seen before, all is well
			if (attribute.DiscreteValues.IndexOf(value) >= 0)
			{
				return;
			}

			//If the value hasn't been seen before and there is room for it, add it.
			if (attribute.DiscreteValues.Length < attribute.DiscreteValueCount)
			{
				double[] discreteValues = attribute.DiscreteValues;

				Array.Resize(ref discreteValues, attribute.DiscreteValues.Length + 1);

				discreteValues[discreteValues.Length - 1] = value;

				attribute.DiscreteValues = discreteValues;
			}
			//Otherwise, it is an error.
			else
			{
				throw new InvalidOperationException(
					"Too many distinct values seen for discrete attribute.  Existing values are " +
					attribute.DiscreteValues + ", new value is " + value);
			}
		}

		/// <summary>
		/// Asserts that the actual value falls within the range of 
		/// acceptable values.
		/// </summary>
		/// <param name="attributeName"></param>
		/// <param name="acceptableValues"></param>
		/// <param name="valueIndex"></param>
		private static void ValidateSet(string attributeName, string[] acceptableValues, double valueIndex)
		{
			//The instance doesn't actually have any string values, it only contains an index to the real value,
			//so just make sure the index corresponds to a valid value.
			if (valueIndex < 0 || valueIndex > acceptableValues.Length -1)
			{
				throw (new InvalidOperationException("Set value index '" + ((int)valueIndex) + "' for attribute '" + attributeName + "' not allowed by attribute definition."));
			}
		}

		#endregion

		#region Private Methods

		/// <summary>
		/// Called before an item is added to the dataset..
		/// This method performs validation to insure all the instance
		/// values are valid according to the attributes defined.
		/// </summary>
		/// <remarks>
		/// If the item is already in another dataset, the item
		/// is copied, and the argument is updated.
		/// </remarks>
		/// <param name="item"></param>
		/// <param name="index"></param>
		private void ItemAdding(ref Instance item, int index)
		{
			//If the item already belongs to a DataSet, create a new copy of it.
			if (item.DataSet != null)
			{
				item = new Instance(item.Label, item.Values);
			}

			if (item.Values.Length != mAttributes.Count)
			{
				throw (new InvalidOperationException(string.Format("Instance has {0} values, expected {1}.", mAttributes.Count,
				                                                   item.Values.Length)));
			}

			for (int i = 0; i < item.Values.Length; i++)
			{
				ValidateAttributeValue(mAttributes[i], item.Values[i]);
			}

			item.DataSet = this;
		}

		/// <summary>
		/// Called whenever an instance is removed from the dataset, it removes
		/// the instance from the dataset so that it can be added to another dataset.
		/// </summary>
		/// <param name="item"></param>
		/// <param name="index"></param>
		private void ItemRemoved(Instance item, int index)
		{
			item.DataSet = null;
		}

		/// <summary>
		/// Callback that is invoked when an attribute is removed.
		/// </summary>
		/// <param name="attribute"></param>
		/// <param name="index"></param>
		private void AttributeRemoved(DataAttribute attribute, int index)
		{
			foreach (Instance instance in Instances)
			{
				double[] values = new double[instance.Values.Length-1];

				//Copy values before the removed index.
				for (int i=0; i < index; i++)
				{
					values[i] = instance.Values[i];
				}

				//Copy values after the index.
				for (int i = index; i < instance.Values.Length - 1; i++)
				{
					values[i] = instance.Values[i + 1];
				}

				//Replace the values
				instance.Values = values;
			}

			//If the index was before the target attribute index, shift it
			if (index < TargetAttributeIndex)
			{
				TargetAttributeIndex--;
			}
			//otherwise, if the index *was* the target attribute, set it back to -1
			else if (index == TargetAttributeIndex)
			{
				TargetAttributeIndex = -1;
			}
		}

		/// <summary>
		/// Callback that is invoked when an attribute is added
		/// to the attribute list.
		/// </summary>
		/// <param name="attribute"></param>
		/// <param name="index"></param>
		private void AttributedAdded(DataAttribute attribute, int index)
		{
			foreach (Instance instance in Instances)
			{
				//Make space for the new value.
				double[] values = new double[instance.Values.Length + 1];

				//Copy in the values before the index.
				for (int i=0; i < index; i++)
				{
					values[i] = instance.Values[i];
				}

				//Set the new value to NaN
				values[index] = double.NaN;

				//Copy in the values after the index
				for (int i=index; i < instance.Values.Length; i++)
				{
					values[i + 1] = instance.Values[i];
				}

				instance.Values = values;
			}

			if (index <= TargetAttributeIndex)
			{
				TargetAttributeIndex++;
			}
		}

		#endregion

		#region Public Properties

		/// <summary>
		/// Gets the attributes present in the data.
		/// </summary>
		public IList<DataAttribute> Attributes
		{
			get { return mAttributes; }
		}

		/// <summary>
		/// Gets the instances of the data set
		/// </summary>
		public IList<Instance> Instances
		{
			get { return mInstances; }
		}

		/// <summary>
		/// The index of the target attribute in the data set.
		/// </summary>
		public int TargetAttributeIndex { get; set; }

		#endregion

		#region Public Constructors

		/// <summary>
		/// Instantiates the data set.
		/// </summary>
		public BasicDataSet()
		{
			TargetAttributeIndex = int.MinValue;

			NotifyingList<DataAttribute> attributes = new NotifyingList<DataAttribute>();
			attributes.ItemAdded += AttributedAdded;
			attributes.ItemRemoved += AttributeRemoved;

			mAttributes = attributes;

			NotifyingList<Instance> instances = new NotifyingList<Instance>();

			instances.ItemAdding += ItemAdding;
			instances.ItemRemoved += ItemRemoved;

			mInstances = instances;
		}

		#endregion

		#region Overrides

		/// <summary>
		/// Creates a console-friendly representation of the data set's contents.
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
			StringBuilder sb = new StringBuilder();

			foreach (Instance instance in Instances)
			{
				sb.AppendLine(instance.ToString());
			}

			return sb.ToString();
		}

		#endregion
	}
}
